前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

单线程的 JavaScript 如何管理任务|原理篇

文章学习导航
面试总览

30 秒速记

  • 核心判断:JavaScript 单线程描述的是执行代码的主线程,异步能力来自宿主环境、任务队列与事件循环协作
  • 原理主线:围绕 「同步任务与异步任务」、「同步任务与函数调用栈」、「异步任务与回调队列」 建立输入、状态变化与输出之间的因果关系
  • 文章范围:解析了 JavaScript 单线程模型,介绍了同步任务与异步任务的区别、函数调用栈的工作原理,以及浏览器如何高效管理和执行各类任务,帮助读者理解 JavaScript 任务调度机制和性能优化思路。
  • 边界与代价:浏览器与 Node.js 的阶段、微任务清空时机并不完全相同,长任务仍会阻塞渲染和输入
  • 工程落地:排障时同时观察调用栈、微任务、宏任务和渲染机会,不能只背固定输出顺序

JavaScript 的单线程指主线程一次只执行一个任务,异步能力则由宿主环境、任务队列和事件循环配合提供。 同步代码进入调用栈顺序执行,定时器、网络请求或用户事件完成后,其回调才会进入相应队列,等调用栈清空后再被取出。每个宏任务结束后会清空微任务队列,浏览器还可能在下一个宏任务前更新界面。长任务仍会阻塞交互,计时器也可能因队列拥堵而延迟;Node.js 的阶段划分及 Node.js 11 前后的微任务处理差异也要结合版本判断。

这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。

版本校准: 原理文章中的代码代表特定实现与写作时间。应用到当前项目时,应先确认浏览器、框架或工具的主版本,再区分稳定的规范语义、可变化的内部实现和项目自身约束。

要怎么理解 JavaScript 是单线程这个概念呢?大概需要从浏览器来说起。

JavaScript 最初被设计为浏览器脚本语言,主要用途包括对页面的操作、与浏览器的交互、与用户的交互、页面逻辑处理等。如果将 JavaScript 设计为多线程,那当多个线程同时对同一个 DOM 节点进行操作时,线程间的同步问题会变得很复杂。

因此,为了避免复杂性,JavaScript 被设计为单线程。

这样一个单线程的 JavaScript,意味着任务需要一个接一个地处理。如果有一个任务是等待用户输入,那在用户进行操作前,所有其他任务都处于等待状态,页面会进入假死状态,用户体验会很糟糕。

那么,为了高效进行页面的交互和渲染处理,我们围绕着任务执行是否阻塞 JavaScript 主线程,将 JavaScript 中的任务分为同步任务和异步任务

# 同步任务与异步任务

  • 同步任务:在主线程上排队执行的任务,前一个任务完整地执行完成后,后一个任务才会被执行
  • 异步任务:不会阻塞主线程,在其任务执行完成之后,会再根据一定的规则去执行相关的回调。

我们先来看一下同步任务在浏览器中的是怎样执行的。

# 同步任务与函数调用栈

JavaScript 在执行过程中每进入一个不同的运行环境时,都会创建一个相应的执行上下文。那么,当我们执行一段 JavaScript 代码时,通常会创建多个执行上下文。

而 JavaScript 解释器会以栈的方式管理这些执行上下文、以及函数之间的调用关系,形成函数调用栈(call stack)(调用栈可理解为一个存储函数调用的栈结构,遵循 FILO(先进后出)的原则)

我们来看一下 JavaScript 中代码执行的过程:

  • 首先进入全局环境,全局执行上下文被创建并添加进栈中;
  • 每调用一个函数,该函数执行上下文会被添加进调用栈,并开始执行;
  • 如果正在调用栈中执行的 A 函数还调用了 B 函数,那么 B 函数也将会被添加进调用栈;
  • 一旦 B 函数被调用,便会立即执行;
  • 当前函数执行完毕后,JavaScript 解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。

由此可见,JavaScript 代码执行过程中,函数调用栈栈底永远是全局执行上下文,栈顶永远是当前执行上下文。

在不考虑全局执行上下文时,我们可以理解为刚开始的时候调用栈是空的,每当有函数被调用,相应的执行上下文都会被添加到调用栈中。执行完函数中相关代码后,该执行上下文又会自动被调用栈移除,最后调用栈又回到了空的状态(同样不考虑全局执行上下文)。

由于栈的容量是有限制的,所以当我们没有合理调用函数的时候,可能会导致爆栈异常,此时控制台便会抛出错误:

这样的一个函数调用栈结构,可以理解为 JavaScript 中同步任务的执行环境,同步任务也可以理解为 JavaScript 代码片段的执行。

同步任务的执行会阻塞主线程,也就是说,一个函数执行的时候不会被抢占,只有在它执行完毕之后,才会去执行任何其他的代码。这意味着如果我们一个任务执行的时间过长,浏览器就无法处理与用户的交互,例如点击或滚动。

因此,我们还需要用到异步任务。

# 异步任务与回调队列

异步任务包括一些需要等待响应的任务,包括用户交互、HTTP 请求、定时器等。

我们知道,I/O 类型的任务会有较长的等待时间,对于这类无法立刻得到结果的事件,可以使用异步任务的方式。这个过程中 JavaScript 线程就不用处于等待状态,CPU 也可以处理其他任务。

异步任务需要提供回调函数,当异步任务有了运行结果之后,该任务则会被添加到回调队列中,主线程在适当的时候会从回调队列中取出相应的回调函数并执行。

这里提到的回调队列又是什么呢?

实际上,JavaScript 在运行的时候,除了函数调用栈之外,还包含了一个待处理的回调队列。在回调队列中的都是已经有了运行结果的异步任务,每一个异步任务都会关联着一个回调函数。

回调队列则遵循 FIFO(先进先出)的原则,JavaScript 执行代码过程中,会进行以下的处理:

  • 运行时,会从最先进入队列的任务开始,处理队列中的任务;
  • 被处理的任务会被移出队列,该任务的运行结果会作为输入参数,并调用与之关联的函数,此时会产生一个函数调用栈;
  • 函数会一直处理到调用栈再次为空,然后 Event Loop 将会处理队列中的下一个任务。

这里我们提到了 Event Loop,它主要是用来管理单线程的 JavaScript 中同步任务和异步任务的执行问题。

# 单线程的 JavaScript 是如何管理任务的

我们知道,单线程的设计会存在阻塞问题,为此 JavaScript 中任务被分为同步和异步任务。那么,同步任务和异步任务之间是按照什么顺序来执行的呢?

JavaScript 有一个基于事件循环的并发模型,称为事件循环(Event Loop),它的设计解决了同步任务和异步任务的管理问题。

根据 JavaScript 运行环境的不同,Event Loop 也会被分成浏览器的 Event Loop 和 Node.js 中的 Event Loop。

# 浏览器的 Event Loop

在浏览器里,每当一个被监听的事件发生时,事件监听器绑定的相关任务就会被添加进回调队列。通过事件产生的任务是异步任务,常见的事件任务包括:

  • 用户交互事件产生的事件任务,比如输入操作;
  • 计时器产生的事件任务,比如setTimeout;
  • 异步请求产生的事件任务,比如 HTTP 请求。

如图,主线程运行的时候,会产生堆(heap)和栈(stack),其中堆为内存、栈为函数调用栈。我们能看到,Event Loop 负责执行代码、收集和处理事件以及执行队列中的子任务,具体包括以下过程

  • JavaScript 有一个主线程和调用栈,所有的任务最终都会被放到调用栈等待主线程执行。
  • 同步任务会被放在调用栈中,按照顺序等待主线程依次执行。
  • 主线程之外存在一个回调队列,回调队列中的异步任务最终会在主线程中以调用栈的方式运行。
  • 同步任务都在主线程上执行,栈中代码在执行的时候会调用浏览器的 API,此时会产生一些异步任务。
  • 异步任务会在有了结果(比如被监听的事件发生时)后,将异步任务以及关联的回调函数放入回调队列中。
  • 调用栈中任务执行完毕后,此时主线程处于空闲状态,会从回调队列中获取任务进行处理。

上述过程会不断重复,这就是 JavaScript 的运行机制,称为事件循环机制(Event Loop)。

Event Loop 的设计会带来一些问题,比如setTimeout、setInterval的时间精确性。这两个方法会设置一个计时器,当计时器计时完成,需要执行回调函数,此时才把回调函数放入回调队列中。

如果当回调函数放入队列时,假设队列中还有大量的回调函数在等待执行,此时就会造成任务执行时间不精确。

要优化这个问题,可以使用系统时钟来补偿计时器的不准确性,从而提升精确度。举个例子,如果你的计时器会在回调时触发二次计时,可以在每次回调任务结束的时候,根据最初的系统时间和该任务的执行时间进行差值比较,来修正后续的计时器时间。

# Node.js 中的 Event Loop

除了浏览器,Node.js 中同样存在 Event Loop。由于 JavaScript 是单线程的,Event Loop 的设计使 Node.js 可以通过将操作转移到系统内核中,来执行非阻塞 I/O 操作

Node.js 中的事件循环执行过程为:

  • 当 Node.js 启动时将初始化事件循环,处理提供的输入脚本;
  • 提供的输入脚本可以进行异步 API 调用,然后开始处理事件循环;
  • 在事件循环的每次运行之间,Node.js 会检查它是否正在等待任何异步 I/O 或计时器,如果没有,则将其干净地关闭

与浏览器不一样,Node.js 中事件循环分成不同的阶段:

   ┌───────────────────────────┐

┌─>│           timers          │

│  └─────────────┬─────────────┘

│  ┌─────────────┴─────────────┐

│  │     pending callbacks     │

│  └─────────────┬─────────────┘

│  ┌─────────────┴─────────────┐

│  │       idle, prepare       │

│  └─────────────┬─────────────┘      ┌───────────────┐

│  ┌─────────────┴─────────────┐      │   incoming:   │

│  │           poll            │<─────┤               |

│  └─────────────┬─────────────┘      │   data, etc.  │

│  ┌─────────────┴─────────────┐      └───────────────┘

│  │           check           │

│  └─────────────┬─────────────┘

│  ┌─────────────┴─────────────┐

└──┤      close callbacks      │

   └───────────────────────────┘
@前端进阶之旅: 代码已经复制到剪贴板

由于事件循环阶段划分不一致,Node.js 和浏览器在对宏任务和微任务的处理上也不一样

# 宏任务和微任务

事件循环中的异步回调队列有两种:宏任务(MacroTask)和微任务(MicroTask)队列。

什么是宏任务和微任务呢?

  • 宏任务:包括 script 全部代码、setTimeout、setInterval、setImmediate(Node.js)、requestAnimationFrame(浏览器)、I/O 操作、UI 渲染(浏览器),这些代码执行便是宏任务。
  • 微任务:包括process.nextTick(Node.js)、Promise、MutationObserver,这些代码执行便是微任务

← JavaScript 引擎如何执行 JavaScript 代码虚拟DOM原理分析 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合